{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "1e3c39cc",
   "metadata": {},
   "source": [
    "# Neural Receiver for OFDM SIMO Systems"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4577e86a",
   "metadata": {},
   "source": [
    "In this notebook, you will learn how to train a neural receiver that implements OFDM detection.\n",
    "The considered setup is shown in the figure below.\n",
    "As one can see, the neural receiver substitutes channel estimation, equalization, and demapping.\n",
    "It takes as input the post-DFT (discrete Fourier transform) received samples, which form the received resource grid, and computes log-likelihood ratios (LLRs) on the transmitted coded bits.\n",
    "These LLRs are then fed to the outer decoder to reconstruct the transmitted information bits."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04dbfdd8",
   "metadata": {},
   "source": [
    "![System Model]()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ae7f463e",
   "metadata": {},
   "source": [
    "Two baselines are considered for benchmarking, which are shown in the figure above.\n",
    "Both baselines use linear minimum mean square error (LMMSE) equalization and demapping assuming additive white Gaussian noise (AWGN).\n",
    "They differ by how channel estimation is performed:\n",
    "\n",
    "- **Pefect CSI**: Perfect channel state information (CSI) knowledge is assumed.\n",
    "- **LS estimation**: Uses the transmitted pilots to perform least squares (LS) estimation of the channel with nearest-neighbor interpolation.\n",
    "\n",
    "All the considered end-to-end systems use an LDPC outer code from the 5G NR specification, QPSK modulation, and a 3GPP CDL channel model simulated in the frequency domain."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "586b1e0b",
   "metadata": {},
   "source": [
    "* [GPU Configuration and Imports](#GPU-Configuration-and-Imports)\n",
    "* [Simulation Parameters](#Simulation-Parameters)\n",
    "* [Neural Receiver](#Neural-Receiver)\n",
    "* [End-to-end System](#End-to-end-System)\n",
    "* [End-to-end System as a Sionna Block](#End-to-end-System-as-a-Sionna-Block)\n",
    "* [Evaluation of the Baselines](#Evaluation-of-the-Baselines)\n",
    "* [Training the Neural Receiver](#Training-the-Neural-Receiver)\n",
    "* [Evaluation of the Neural Receiver](#Evaluation-of-the-Neural-Receiver)\n",
    "* [Pre-computed Results](#Pre-computed-Results)\n",
    "* [References](#References)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "69d31188",
   "metadata": {},
   "source": [
    "## GPU Configuration and Imports <a class=\"anchor\" id=\"GPU-Configuration-and-Imports\"></a>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "6393b2fe",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:23:31.825365Z",
     "iopub.status.busy": "2025-03-10T22:23:31.824690Z",
     "iopub.status.idle": "2025-03-10T22:23:34.287511Z",
     "shell.execute_reply": "2025-03-10T22:23:34.286518Z"
    }
   },
   "outputs": [],
   "source": [
    "import os\n",
    "if os.getenv(\"CUDA_VISIBLE_DEVICES\") is None:\n",
    "    gpu_num = 0 # Use \"\" to use the CPU\n",
    "    os.environ[\"CUDA_VISIBLE_DEVICES\"] = f\"{gpu_num}\"\n",
    "os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'\n",
    "\n",
    "# Import Sionna\n",
    "try:\n",
    "    import sionna.phy\n",
    "except ImportError as e:\n",
    "    import sys\n",
    "    if 'google.colab' in sys.modules:\n",
    "       # Install Sionna in Google Colab\n",
    "       print(\"Installing Sionna and restarting the runtime. Please run the cell again.\")\n",
    "       os.system(\"pip install sionna\")\n",
    "       os.kill(os.getpid(), 5)\n",
    "    else:\n",
    "       raise e\n",
    "\n",
    "# Configure the notebook to use only a single GPU and allocate only as much memory as needed\n",
    "# For more details, see https://www.tensorflow.org/guide/gpu\n",
    "import tensorflow as tf\n",
    "gpus = tf.config.list_physical_devices('GPU')\n",
    "if gpus:\n",
    "    try:\n",
    "        tf.config.experimental.set_memory_growth(gpus[0], True)\n",
    "    except RuntimeError as e:\n",
    "        print(e)\n",
    "# Avoid warnings from TensorFlow\n",
    "tf.get_logger().setLevel('ERROR')\n",
    "\n",
    "sionna.phy.config.seed = 42 # Set seed for reproducible random number generation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "6244a108",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:23:34.290150Z",
     "iopub.status.busy": "2025-03-10T22:23:34.289733Z",
     "iopub.status.idle": "2025-03-10T22:23:34.308845Z",
     "shell.execute_reply": "2025-03-10T22:23:34.308071Z"
    }
   },
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import pickle\n",
    "\n",
    "from tensorflow.keras import Model\n",
    "from tensorflow.keras.layers import Layer, Conv2D, LayerNormalization\n",
    "from tensorflow.nn import relu\n",
    "\n",
    "from sionna.phy import Block\n",
    "from sionna.phy.channel.tr38901 import Antenna, AntennaArray, CDL\n",
    "from sionna.phy.channel import OFDMChannel\n",
    "from sionna.phy.mimo import StreamManagement\n",
    "from sionna.phy.ofdm import ResourceGrid, ResourceGridMapper, LSChannelEstimator, \\\n",
    "                            LMMSEEqualizer, RemoveNulledSubcarriers, ResourceGridDemapper\n",
    "from sionna.phy.utils import  ebnodb2no, insert_dims, log10, expand_to_rank\n",
    "from sionna.phy.fec.ldpc import LDPC5GEncoder, LDPC5GDecoder\n",
    "from sionna.phy.mapping import Mapper, Demapper, BinarySource\n",
    "from sionna.phy.utils import sim_ber"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10b3af73",
   "metadata": {},
   "source": [
    "## Simulation Parameters <a class=\"anchor\" id=\"Simulation-Parameters\"></a>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "2e2b69eb",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:23:34.311148Z",
     "iopub.status.busy": "2025-03-10T22:23:34.310866Z",
     "iopub.status.idle": "2025-03-10T22:23:34.316890Z",
     "shell.execute_reply": "2025-03-10T22:23:34.316074Z"
    }
   },
   "outputs": [],
   "source": [
    "############################################\n",
    "## Channel configuration\n",
    "carrier_frequency = 3.5e9 # Hz\n",
    "delay_spread = 100e-9 # s\n",
    "cdl_model = \"C\" # CDL model to use\n",
    "speed = 10.0 # Speed for evaluation and training [m/s]\n",
    "# SNR range for evaluation and training [dB]\n",
    "ebno_db_min = -5.0\n",
    "ebno_db_max = 10.0\n",
    "\n",
    "############################################\n",
    "## OFDM waveform configuration\n",
    "subcarrier_spacing = 30e3 # Hz\n",
    "fft_size = 128 # Number of subcarriers forming the resource grid, including the null-subcarrier and the guard bands\n",
    "num_ofdm_symbols = 14 # Number of OFDM symbols forming the resource grid\n",
    "dc_null = True # Null the DC subcarrier\n",
    "num_guard_carriers = [5, 6] # Number of guard carriers on each side\n",
    "pilot_pattern = \"kronecker\" # Pilot pattern\n",
    "pilot_ofdm_symbol_indices = [2, 11] # Index of OFDM symbols carrying pilots\n",
    "cyclic_prefix_length = 0 # Simulation in frequency domain. This is useless\n",
    "\n",
    "############################################\n",
    "## Modulation and coding configuration\n",
    "num_bits_per_symbol = 2 # QPSK\n",
    "coderate = 0.5 # Coderate for LDPC code\n",
    "\n",
    "############################################\n",
    "## Neural receiver configuration\n",
    "num_conv_channels = 128 # Number of convolutional channels for the convolutional layers forming the neural receiver\n",
    "\n",
    "############################################\n",
    "## Training configuration\n",
    "num_training_iterations = 30000 # Number of training iterations\n",
    "training_batch_size = 128 # Training batch size\n",
    "model_weights_path = \"neural_receiver_weights\" # Location to save the neural receiver weights once training is done\n",
    "\n",
    "############################################\n",
    "## Evaluation configuration\n",
    "results_filename = \"neural_receiver_results\" # Location to save the results"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ebbab91e",
   "metadata": {},
   "source": [
    "The `StreamManagement` class is used to configure the receiver-transmitter association and the number of streams per transmitter.\n",
    "A SIMO system is considered, with a single transmitter equipped with a single non-polarized antenna.\n",
    "Therefore, there is only a single stream, and the receiver-transmitter association matrix is $[1]$.\n",
    "The receiver is equipped with an antenna array."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "08378e86",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:23:34.319121Z",
     "iopub.status.busy": "2025-03-10T22:23:34.318837Z",
     "iopub.status.idle": "2025-03-10T22:23:34.322794Z",
     "shell.execute_reply": "2025-03-10T22:23:34.322219Z"
    }
   },
   "outputs": [],
   "source": [
    "stream_manager = StreamManagement(np.array([[1]]), # Receiver-transmitter association matrix\n",
    "                                  1)               # One stream per transmitter"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "feb77719",
   "metadata": {},
   "source": [
    "The `ResourceGrid` class is used to configure the OFDM resource grid. It is initialized with the parameters defined above."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "41b17f86",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:23:34.325076Z",
     "iopub.status.busy": "2025-03-10T22:23:34.324760Z",
     "iopub.status.idle": "2025-03-10T22:23:34.706761Z",
     "shell.execute_reply": "2025-03-10T22:23:34.705633Z"
    }
   },
   "outputs": [],
   "source": [
    "resource_grid = ResourceGrid(num_ofdm_symbols = num_ofdm_symbols,\n",
    "                             fft_size = fft_size,\n",
    "                             subcarrier_spacing = subcarrier_spacing,\n",
    "                             num_tx = 1,\n",
    "                             num_streams_per_tx = 1,\n",
    "                             cyclic_prefix_length = cyclic_prefix_length,\n",
    "                             dc_null = dc_null,\n",
    "                             pilot_pattern = pilot_pattern,\n",
    "                             pilot_ofdm_symbol_indices = pilot_ofdm_symbol_indices,\n",
    "                             num_guard_carriers = num_guard_carriers)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6b61e3e9",
   "metadata": {},
   "source": [
    "Outer coding is performed such that all the databits carried by the resource grid with size `fft_size`x`num_ofdm_symbols` form a single codeword."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "b9c1afbd",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:23:34.709540Z",
     "iopub.status.busy": "2025-03-10T22:23:34.709242Z",
     "iopub.status.idle": "2025-03-10T22:23:34.714891Z",
     "shell.execute_reply": "2025-03-10T22:23:34.714086Z"
    }
   },
   "outputs": [],
   "source": [
    "# Codeword length. It is calculated from the total number of databits carried by the resource grid, and the number of bits transmitted per resource element\n",
    "n = int(resource_grid.num_data_symbols*num_bits_per_symbol)\n",
    "# Number of information bits per codeword\n",
    "k = int(n*coderate)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9a935c71",
   "metadata": {},
   "source": [
    "The SIMO link is setup by considering an uplink transmission with one user terminal (UT) equipped with a single non-polarized antenna, and a base station (BS) equipped with an antenna array.\n",
    "One can try other configurations for the BS antenna array."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "c025cf52",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:23:34.717658Z",
     "iopub.status.busy": "2025-03-10T22:23:34.717329Z",
     "iopub.status.idle": "2025-03-10T22:23:34.735477Z",
     "shell.execute_reply": "2025-03-10T22:23:34.734598Z"
    }
   },
   "outputs": [],
   "source": [
    "ut_antenna = Antenna(polarization=\"single\",\n",
    "                     polarization_type=\"V\",\n",
    "                     antenna_pattern=\"38.901\",\n",
    "                     carrier_frequency=carrier_frequency)\n",
    "\n",
    "bs_array = AntennaArray(num_rows=1,\n",
    "                        num_cols=1,\n",
    "                        polarization=\"dual\",\n",
    "                        polarization_type=\"VH\",\n",
    "                        antenna_pattern=\"38.901\",\n",
    "                        carrier_frequency=carrier_frequency)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6d33937d",
   "metadata": {},
   "source": [
    "## Neural Receiver <a class=\"anchor\" id=\"Neural-Receiver\"></a>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e9651393",
   "metadata": {},
   "source": [
    "The next cell defines the Keras layers that implement the neural receiver.\n",
    "As in [1] and [2], a neural receiver using residual convolutional layers is implemented. Convolutional layers are leveraged to efficienly process the 2D resource grid, that is fed as an input to the neural receiver.\n",
    "Residual (skip) connections are used to avoid gradient vanishing [3].\n",
    "\n",
    "For convinience, a Keras layer that implements a *residual block* is first defined. The Keras layer that implements the neural receiver is built by stacking such blocks. The following figure shows the architecture of the neural receiver."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "63c331fb",
   "metadata": {},
   "source": [
    "![Neural RX]()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "9651cec8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:23:34.738748Z",
     "iopub.status.busy": "2025-03-10T22:23:34.738435Z",
     "iopub.status.idle": "2025-03-10T22:23:34.748868Z",
     "shell.execute_reply": "2025-03-10T22:23:34.748119Z"
    }
   },
   "outputs": [],
   "source": [
    "class ResidualBlock(Layer):\n",
    "    r\"\"\"\n",
    "    This Keras layer implements a convolutional residual block made of two convolutional layers with ReLU activation, layer normalization, and a skip connection.\n",
    "    The number of convolutional channels of the input must match the number of kernel of the convolutional layers ``num_conv_channel`` for the skip connection to work.\n",
    "\n",
    "    Input\n",
    "    ------\n",
    "    : [batch size, num time samples, num subcarriers, num_conv_channel], tf.float\n",
    "        Input of the layer\n",
    "\n",
    "    Output\n",
    "    -------\n",
    "    : [batch size, num time samples, num subcarriers, num_conv_channel], tf.float\n",
    "        Output of the layer\n",
    "    \"\"\"\n",
    "\n",
    "    def build(self, input_shape):\n",
    "\n",
    "        # Layer normalization is done over the last three dimensions: time, frequency, conv 'channels'\n",
    "        self._layer_norm_1 = LayerNormalization(axis=(-1, -2, -3))\n",
    "        self._conv_1 = Conv2D(filters=num_conv_channels,\n",
    "                              kernel_size=[3,3],\n",
    "                              padding='same',\n",
    "                              activation=None)\n",
    "        # Layer normalization is done over the last three dimensions: time, frequency, conv 'channels'\n",
    "        self._layer_norm_2 = LayerNormalization(axis=(-1, -2, -3))\n",
    "        self._conv_2 = Conv2D(filters=num_conv_channels,\n",
    "                              kernel_size=[3,3],\n",
    "                              padding='same',\n",
    "                              activation=None)\n",
    "\n",
    "    def call(self, inputs):\n",
    "        z = self._layer_norm_1(inputs)\n",
    "        z = relu(z)\n",
    "        z = self._conv_1(z)\n",
    "        z = self._layer_norm_2(z)\n",
    "        z = relu(z)\n",
    "        z = self._conv_2(z) # [batch size, num time samples, num subcarriers, num_channels]\n",
    "        # Skip connection\n",
    "        z = z + inputs\n",
    "\n",
    "        return z\n",
    "\n",
    "class NeuralReceiver(Layer):\n",
    "    r\"\"\"\n",
    "    Keras layer implementing a residual convolutional neural receiver.\n",
    "\n",
    "    This neural receiver is fed with the post-DFT received samples, forming a resource grid of size num_of_symbols x fft_size, and computes LLRs on the transmitted coded bits.\n",
    "    These LLRs can then be fed to an outer decoder to reconstruct the information bits.\n",
    "\n",
    "    As the neural receiver is fed with the entire resource grid, including the guard bands and pilots, it also computes LLRs for these resource elements.\n",
    "    They must be discarded to only keep the LLRs corresponding to the data-carrying resource elements.\n",
    "\n",
    "    Input\n",
    "    ------\n",
    "    y : [batch size, num rx antenna, num ofdm symbols, num subcarriers], tf.complex\n",
    "        Received post-DFT samples.\n",
    "\n",
    "    no : [batch size], tf.float32\n",
    "        Noise variance. At training, a different noise variance value is sampled for each batch example.\n",
    "\n",
    "    Output\n",
    "    -------\n",
    "    : [batch size, num ofdm symbols, num subcarriers, num_bits_per_symbol]\n",
    "        LLRs on the transmitted bits.\n",
    "        LLRs computed for resource elements not carrying data (pilots, guard bands...) must be discarded.\n",
    "    \"\"\"\n",
    "\n",
    "    def build(self, input_shape):\n",
    "\n",
    "        # Input convolution\n",
    "        self._input_conv = Conv2D(filters=num_conv_channels,\n",
    "                                  kernel_size=[3,3],\n",
    "                                  padding='same',\n",
    "                                  activation=None)\n",
    "        # Residual blocks\n",
    "        self._res_block_1 = ResidualBlock()\n",
    "        self._res_block_2 = ResidualBlock()\n",
    "        self._res_block_3 = ResidualBlock()\n",
    "        self._res_block_4 = ResidualBlock()\n",
    "        # Output conv\n",
    "        self._output_conv = Conv2D(filters=num_bits_per_symbol,\n",
    "                                   kernel_size=[3,3],\n",
    "                                   padding='same',\n",
    "                                   activation=None)\n",
    "\n",
    "    def call(self, y, no):\n",
    "\n",
    "        # Feeding the noise power in log10 scale helps with the performance\n",
    "        no = log10(no)\n",
    "\n",
    "        # Stacking the real and imaginary components of the different antennas along the 'channel' dimension\n",
    "        y = tf.transpose(y, [0, 2, 3, 1]) # Putting antenna dimension last\n",
    "        no = insert_dims(no, 3, 1)\n",
    "        no = tf.tile(no, [1, y.shape[1], y.shape[2], 1])\n",
    "        # z : [batch size, num ofdm symbols, num subcarriers, 2*num rx antenna + 1]\n",
    "        z = tf.concat([tf.math.real(y),\n",
    "                       tf.math.imag(y),\n",
    "                       no], axis=-1)\n",
    "        # Input conv\n",
    "        z = self._input_conv(z)\n",
    "        # Residual blocks\n",
    "        z = self._res_block_1(z)\n",
    "        z = self._res_block_2(z)\n",
    "        z = self._res_block_3(z)\n",
    "        z = self._res_block_4(z)\n",
    "        # Output conv\n",
    "        z = self._output_conv(z)\n",
    "\n",
    "        return z"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5ef365bd",
   "metadata": {},
   "source": [
    "## End-to-end System <a class=\"anchor\" id=\"End-to-end-System\"></a>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1cf89203",
   "metadata": {},
   "source": [
    "The following cell defines the end-to-end system.\n",
    "\n",
    "Training is done on the bit-metric decoding (BMD) rate which is computed from the transmitted bits and LLRs:\n",
    "\n",
    "\\begin{equation}\n",
    "R = 1 - \\frac{1}{SNMK} \\sum_{s = 0}^{S-1} \\sum_{n = 0}^{N-1} \\sum_{m = 0}^{M-1} \\sum_{k = 0}^{K-1} \\texttt{BCE} \\left( B_{s,n,m,k}, \\texttt{LLR}_{s,n,m,k} \\right)\n",
    "\\end{equation}\n",
    "\n",
    "where\n",
    "\n",
    "* $S$ is the batch size\n",
    "* $N$ the number of subcarriers\n",
    "* $M$ the number of OFDM symbols\n",
    "* $K$ the number of bits per symbol\n",
    "* $B_{s,n,m,k}$ the $k^{th}$ coded bit transmitted on the resource element $(n,m)$ and for the $s^{th}$ batch example\n",
    "* $\\texttt{LLR}_{s,n,m,k}$ the LLR (logit) computed by the neural receiver corresponding to the $k^{th}$ coded bit transmitted on the resource element $(n,m)$ and for the $s^{th}$ batch example\n",
    "* $\\texttt{BCE} \\left( \\cdot, \\cdot \\right)$ the binary cross-entropy in log base 2\n",
    "\n",
    "Because no outer code is required at training, the outer encoder and decoder are not used at training to reduce computational complexity.\n",
    "\n",
    "The BMD rate is known to be an achievable information rate for BICM systems, which motivates its used as objective function [4]."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "07bc54fd",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:23:34.752335Z",
     "iopub.status.busy": "2025-03-10T22:23:34.752139Z",
     "iopub.status.idle": "2025-03-10T22:23:34.853665Z",
     "shell.execute_reply": "2025-03-10T22:23:34.852819Z"
    }
   },
   "outputs": [],
   "source": [
    "## Transmitter\n",
    "binary_source = BinarySource()\n",
    "mapper = Mapper(\"qam\", num_bits_per_symbol)\n",
    "rg_mapper = ResourceGridMapper(resource_grid)\n",
    "\n",
    "## Channel\n",
    "cdl = CDL(cdl_model, delay_spread, carrier_frequency,\n",
    "          ut_antenna, bs_array, \"uplink\", min_speed=speed)\n",
    "channel = OFDMChannel(cdl, resource_grid, normalize_channel=True, return_channel=True)\n",
    "\n",
    "## Receiver\n",
    "neural_receiver = NeuralReceiver()\n",
    "rg_demapper = ResourceGridDemapper(resource_grid, stream_manager) # Used to extract data-carrying resource elements"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2ccac0ab",
   "metadata": {},
   "source": [
    "The following cell performs one forward step through the end-to-end system:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "47b41c7b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:23:34.858009Z",
     "iopub.status.busy": "2025-03-10T22:23:34.857798Z",
     "iopub.status.idle": "2025-03-10T22:23:38.914441Z",
     "shell.execute_reply": "2025-03-10T22:23:38.913626Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "c shape:  (64, 1, 1, 2784)\n",
      "x shape:  (64, 1, 1, 1392)\n",
      "x_rg shape:  (64, 1, 1, 14, 128)\n",
      "y shape:  (64, 1, 2, 14, 128)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "llr shape:  (64, 14, 128, 2)\n",
      "Post RG-demapper LLRs:  (64, 1, 1, 2784)\n"
     ]
    }
   ],
   "source": [
    "batch_size = 64\n",
    "ebno_db = tf.fill([batch_size], 5.0)\n",
    "no = ebnodb2no(ebno_db, num_bits_per_symbol, coderate)\n",
    "\n",
    "\n",
    "## Transmitter\n",
    "# Generate codewords\n",
    "c = binary_source([batch_size, 1, 1, n])\n",
    "print(\"c shape: \", c.shape)\n",
    "# Map bits to QAM symbols\n",
    "x = mapper(c)\n",
    "print(\"x shape: \", x.shape)\n",
    "# Map the QAM symbols to a resource grid\n",
    "x_rg = rg_mapper(x)\n",
    "print(\"x_rg shape: \", x_rg.shape)\n",
    "\n",
    "######################################\n",
    "## Channel\n",
    "# A batch of new channel realizations is sampled and applied at every inference\n",
    "no_ = expand_to_rank(no, tf.rank(x_rg))\n",
    "y,_ = channel(x_rg, no_)\n",
    "print(\"y shape: \", y.shape)\n",
    "\n",
    "######################################\n",
    "## Receiver\n",
    "# The neural receiver computes LLRs from the frequency domain received symbols and N0\n",
    "y = tf.squeeze(y, axis=1)\n",
    "llr = neural_receiver(y, no)\n",
    "print(\"llr shape: \", llr.shape)\n",
    "# Reshape the input to fit what the resource grid demapper is expected\n",
    "llr = insert_dims(llr, 2, 1)\n",
    "# Extract data-carrying resource elements. The other LLRs are discarded\n",
    "llr = rg_demapper(llr)\n",
    "llr = tf.reshape(llr, [batch_size, 1, 1, n])\n",
    "print(\"Post RG-demapper LLRs: \", llr.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7308c9f5",
   "metadata": {},
   "source": [
    "The BMD rate is computed from the LLRs and transmitted bits as follows:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "752452bc",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:23:38.918133Z",
     "iopub.status.busy": "2025-03-10T22:23:38.917834Z",
     "iopub.status.idle": "2025-03-10T22:23:39.137123Z",
     "shell.execute_reply": "2025-03-10T22:23:39.136301Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Rate: -7.58E-01 bit\n"
     ]
    }
   ],
   "source": [
    "bce = tf.nn.sigmoid_cross_entropy_with_logits(c, llr)\n",
    "bce = tf.reduce_mean(bce)\n",
    "rate = tf.constant(1.0, tf.float32) - bce/tf.math.log(2.)\n",
    "print(f\"Rate: {rate:.2E} bit\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a24c399e",
   "metadata": {},
   "source": [
    "The rate is very poor (negative values means 0 bit) as the neural receiver is not trained."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d0b7969a",
   "metadata": {},
   "source": [
    "## End-to-end System as a Sionna Block"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cafb2661",
   "metadata": {},
   "source": [
    "The following Sionna block implements the three considered end-to-end systems (perfect CSI baseline, LS estimation baseline, and neural receiver).\n",
    "\n",
    "When instantiating the end-to-end model, the parameter ``system`` is used to specify the system to setup, and the parameter ``training`` is used to specified if the system is instantiated to be trained or to be evaluated. The ``training`` parameter is only relevant when the neural receiver is used.\n",
    "\n",
    "At each call of this model:\n",
    "\n",
    "* A batch of codewords is randomly sampled, modulated, and mapped to resource grids to form the channel inputs\n",
    "* A batch of channel realizations is randomly sampled and applied to the channel inputs\n",
    "* The receiver is executed on the post-DFT received samples to compute LLRs on the coded bits.\n",
    "  Which receiver is executed (baseline with perfect CSI knowledge, baseline with LS estimation, or neural receiver) depends on the specified ``system`` parameter.\n",
    "* If not training, the outer decoder is applied to reconstruct the information bits\n",
    "* If training, the BMD rate is estimated over the batch from the LLRs and the transmitted bits\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "f6621847",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:23:39.141334Z",
     "iopub.status.busy": "2025-03-10T22:23:39.141116Z",
     "iopub.status.idle": "2025-03-10T22:23:39.154962Z",
     "shell.execute_reply": "2025-03-10T22:23:39.154304Z"
    }
   },
   "outputs": [],
   "source": [
    "class E2ESystem(Block):\n",
    "    r\"\"\"\n",
    "    Sionna Block that implements the end-to-end system\n",
    "\n",
    "    As the three considered end-to-end systems (perfect CSI baseline, LS estimation baseline, and neural receiver) share most of\n",
    "    the link components (transmitter, channel model, outer code...), they are implemented using the same end-to-end model.\n",
    "\n",
    "    When instantiating the Sionna block, the parameter ``system`` is used to specify the system to setup,\n",
    "    and the parameter ``training`` is used to specified if the system is instantiated to be trained or to be evaluated.\n",
    "    The ``training`` parameter is only relevant when the neural\n",
    "\n",
    "    At each call of this model:\n",
    "    * A batch of codewords is randomly sampled, modulated, and mapped to resource grids to form the channel inputs\n",
    "    * A batch of channel realizations is randomly sampled and applied to the channel inputs\n",
    "    * The receiver is executed on the post-DFT received samples to compute LLRs on the coded bits.\n",
    "      Which receiver is executed (baseline with perfect CSI knowledge, baseline with LS estimation, or neural receiver) depends\n",
    "      on the specified ``system`` parameter.\n",
    "    * If not training, the outer decoder is applied to reconstruct the information bits\n",
    "    * If training, the BMD rate is estimated over the batch from the LLRs and the transmitted bits\n",
    "\n",
    "    Parameters\n",
    "    -----------\n",
    "    system : str\n",
    "        Specify the receiver to use. Should be one of 'baseline-perfect-csi', 'baseline-ls-estimation' or 'neural-receiver'\n",
    "\n",
    "    training : bool\n",
    "        Set to `True` if the system is instantiated to be trained. Set to `False` otherwise. Defaults to `False`.\n",
    "        If the system is instantiated to be trained, the outer encoder and decoder are not instantiated as they are not required for training.\n",
    "        This significantly reduces the computational complexity of training.\n",
    "        If training, the bit-metric decoding (BMD) rate is computed from the transmitted bits and the LLRs. The BMD rate is known to be\n",
    "        an achievable information rate for BICM systems, and therefore training of the neural receiver aims at maximizing this rate.\n",
    "\n",
    "    Input\n",
    "    ------\n",
    "    batch_size : int\n",
    "        Batch size\n",
    "\n",
    "    no : scalar or [batch_size], tf.float\n",
    "        Noise variance.\n",
    "        At training, a different noise variance should be sampled for each batch example.\n",
    "\n",
    "    Output\n",
    "    -------\n",
    "    If ``training`` is set to `True`, then the output is a single scalar, which is an estimation of the BMD rate computed over the batch. It\n",
    "    should be used as objective for training.\n",
    "    If ``training`` is set to `False`, the transmitted information bits and their reconstruction on the receiver side are returned to\n",
    "    compute the block/bit error rate.\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, system, training=False):\n",
    "        super().__init__()\n",
    "        self._system = system\n",
    "        self._training = training\n",
    "\n",
    "        ######################################\n",
    "        ## Transmitter\n",
    "        self._binary_source = BinarySource()\n",
    "        # To reduce the computational complexity of training, the outer code is not used when training,\n",
    "        # as it is not required\n",
    "        if not training:\n",
    "            self._encoder = LDPC5GEncoder(k, n)\n",
    "        self._mapper = Mapper(\"qam\", num_bits_per_symbol)\n",
    "        self._rg_mapper = ResourceGridMapper(resource_grid)\n",
    "\n",
    "        ######################################\n",
    "        ## Channel\n",
    "        # A 3GPP CDL channel model is used\n",
    "        cdl = CDL(cdl_model, delay_spread, carrier_frequency,\n",
    "                  ut_antenna, bs_array, \"uplink\", min_speed=speed)\n",
    "        self._channel = OFDMChannel(cdl, resource_grid, normalize_channel=True, return_channel=True)\n",
    "\n",
    "        ######################################\n",
    "        ## Receiver\n",
    "        # Three options for the receiver depending on the value of `system`\n",
    "        if \"baseline\" in system:\n",
    "            if system == 'baseline-perfect-csi': # Perfect CSI\n",
    "                self._removed_null_subc = RemoveNulledSubcarriers(resource_grid)\n",
    "            elif system == 'baseline-ls-estimation': # LS estimation\n",
    "                self._ls_est = LSChannelEstimator(resource_grid, interpolation_type=\"nn\")\n",
    "            # Components required by both baselines\n",
    "            self._lmmse_equ = LMMSEEqualizer(resource_grid, stream_manager, )\n",
    "            self._demapper = Demapper(\"app\", \"qam\", num_bits_per_symbol)\n",
    "        elif system == \"neural-receiver\": # Neural receiver\n",
    "            self._neural_receiver = NeuralReceiver()\n",
    "            self._rg_demapper = ResourceGridDemapper(resource_grid, stream_manager) # Used to extract data-carrying resource elements\n",
    "        # To reduce the computational complexity of training, the outer code is not used when training,\n",
    "        # as it is not required\n",
    "        if not training:\n",
    "            self._decoder = LDPC5GDecoder(self._encoder, hard_out=True)\n",
    "\n",
    "    @tf.function\n",
    "    def call(self, batch_size, ebno_db):\n",
    "\n",
    "        # If `ebno_db` is a scalar, a tensor with shape [batch size] is created as it is what is expected by some layers\n",
    "        if len(ebno_db.shape) == 0:\n",
    "            ebno_db = tf.fill([batch_size], ebno_db)\n",
    "\n",
    "        ######################################\n",
    "        ## Transmitter\n",
    "        no = ebnodb2no(ebno_db, num_bits_per_symbol, coderate)\n",
    "        # Outer coding is only performed if not training\n",
    "        if self._training:\n",
    "            c = self._binary_source([batch_size, 1, 1, n])\n",
    "        else:\n",
    "            b = self._binary_source([batch_size, 1, 1, k])\n",
    "            c = self._encoder(b)\n",
    "        # Modulation\n",
    "        x = self._mapper(c)\n",
    "        x_rg = self._rg_mapper(x)\n",
    "\n",
    "        ######################################\n",
    "        ## Channel\n",
    "        # A batch of new channel realizations is sampled and applied at every inference\n",
    "        no_ = expand_to_rank(no, tf.rank(x_rg))\n",
    "        y,h = self._channel(x_rg, no_)\n",
    "\n",
    "        ######################################\n",
    "        ## Receiver\n",
    "        # Three options for the receiver depending on the value of ``system``\n",
    "        if \"baseline\" in self._system:\n",
    "            if self._system == 'baseline-perfect-csi':\n",
    "                h_hat = self._removed_null_subc(h) # Extract non-null subcarriers\n",
    "                err_var = 0.0 # No channel estimation error when perfect CSI knowledge is assumed\n",
    "            elif self._system == 'baseline-ls-estimation':\n",
    "                h_hat, err_var = self._ls_est(y, no) # LS channel estimation with nearest-neighbor\n",
    "            x_hat, no_eff = self._lmmse_equ(y, h_hat, err_var, no) # LMMSE equalization\n",
    "            no_eff_= expand_to_rank(no_eff, tf.rank(x_hat))\n",
    "            llr = self._demapper(x_hat, no_eff_) # Demapping\n",
    "        elif self._system == \"neural-receiver\":\n",
    "            # The neural receiver computes LLRs from the frequency domain received symbols and N0\n",
    "            y = tf.squeeze(y, axis=1)\n",
    "            llr = self._neural_receiver(y, no)\n",
    "            llr = insert_dims(llr, 2, 1) # Reshape the input to fit what the resource grid demapper is expected\n",
    "            llr = self._rg_demapper(llr) # Extract data-carrying resource elements. The other LLrs are discarded\n",
    "            llr = tf.reshape(llr, [batch_size, 1, 1, n]) # Reshape the LLRs to fit what the outer decoder is expected\n",
    "\n",
    "        # Outer coding is not needed if the information rate is returned\n",
    "        if self._training:\n",
    "            # Compute and return BMD rate (in bit), which is known to be an achievable\n",
    "            # information rate for BICM systems.\n",
    "            # Training aims at maximizing the BMD rate\n",
    "            bce = tf.nn.sigmoid_cross_entropy_with_logits(c, llr)\n",
    "            bce = tf.reduce_mean(bce)\n",
    "            rate = tf.constant(1.0, tf.float32) - bce/tf.math.log(2.)\n",
    "            return rate\n",
    "        else:\n",
    "            # Outer decoding\n",
    "            b_hat = self._decoder(llr)\n",
    "            return b,b_hat # Ground truth and reconstructed information bits returned for BER/BLER computation"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9d3f56d8",
   "metadata": {},
   "source": [
    "## Evaluation of the Baselines <a class=\"anchor\" id=\"Evaluation-of-the-Baselines\"></a>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "35f3fcb8",
   "metadata": {},
   "source": [
    "We evaluate the BERs achieved by the baselines in the next cell.\n",
    "\n",
    "**Note:** Evaluation of the two systems can take a while. Therefore, we provide pre-computed results at the end of this notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "39bb18d5",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:23:39.158774Z",
     "iopub.status.busy": "2025-03-10T22:23:39.158479Z",
     "iopub.status.idle": "2025-03-10T22:23:39.162153Z",
     "shell.execute_reply": "2025-03-10T22:23:39.161268Z"
    }
   },
   "outputs": [],
   "source": [
    "# Range of SNRs over which the systems are evaluated\n",
    "ebno_dbs = np.arange(ebno_db_min, # Min SNR for evaluation\n",
    "                     ebno_db_max, # Max SNR for evaluation\n",
    "                     0.5) # Step"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "39ba8f2c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:23:39.165471Z",
     "iopub.status.busy": "2025-03-10T22:23:39.165222Z",
     "iopub.status.idle": "2025-03-10T22:25:22.149244Z",
     "shell.execute_reply": "2025-03-10T22:25:22.148191Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "EbNo [dB] |        BER |       BLER |  bit errors |    num bits | block errors |  num blocks | runtime [s] |    status\n",
      "---------------------------------------------------------------------------------------------------------------------------------------\n",
      "     -5.0 | 2.5209e-01 | 1.0000e+00 |       44916 |      178176 |          128 |         128 |         9.4 |reached target block errors\n",
      "     -4.5 | 2.3712e-01 | 1.0000e+00 |       42249 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -4.0 | 2.1769e-01 | 1.0000e+00 |       38787 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -3.5 | 1.9162e-01 | 1.0000e+00 |       34142 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -3.0 | 1.5846e-01 | 1.0000e+00 |       28233 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -2.5 | 1.0702e-01 | 9.9219e-01 |       19068 |      178176 |          127 |         128 |         0.1 |reached target block errors\n",
      "     -2.0 | 1.6891e-02 | 5.1172e-01 |        6019 |      356352 |          131 |         256 |         0.2 |reached target block errors\n",
      "     -1.5 | 8.1623e-04 | 2.6562e-02 |        4363 |     5345280 |          102 |        3840 |         2.4 |reached target block errors\n",
      "     -1.0 | 1.3958e-04 | 2.1875e-03 |        2487 |    17817600 |           28 |       12800 |         8.0 |reached max iterations\n",
      "     -0.5 | 1.2628e-05 | 1.5625e-04 |         225 |    17817600 |            2 |       12800 |         8.0 |reached max iterations\n",
      "      0.0 | 1.5266e-05 | 1.5625e-04 |         272 |    17817600 |            2 |       12800 |         8.0 |reached max iterations\n",
      "      0.5 | 1.4592e-06 | 7.8125e-05 |          26 |    17817600 |            1 |       12800 |         8.0 |reached max iterations\n",
      "      1.0 | 0.0000e+00 | 0.0000e+00 |           0 |    17817600 |            0 |       12800 |         8.0 |reached max iterations\n",
      "\n",
      "Simulation stopped as no error occurred @ EbNo = 1.0 dB.\n",
      "\n",
      "EbNo [dB] |        BER |       BLER |  bit errors |    num bits | block errors |  num blocks | runtime [s] |    status\n",
      "---------------------------------------------------------------------------------------------------------------------------------------\n",
      "     -5.0 | 3.9043e-01 | 1.0000e+00 |       69565 |      178176 |          128 |         128 |         3.0 |reached target block errors\n",
      "     -4.5 | 3.7739e-01 | 1.0000e+00 |       67241 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -4.0 | 3.6709e-01 | 1.0000e+00 |       65406 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -3.5 | 3.5535e-01 | 1.0000e+00 |       63315 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -3.0 | 3.4154e-01 | 1.0000e+00 |       60855 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -2.5 | 3.2643e-01 | 1.0000e+00 |       58162 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -2.0 | 3.1102e-01 | 1.0000e+00 |       55416 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -1.5 | 2.9498e-01 | 1.0000e+00 |       52558 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -1.0 | 2.7463e-01 | 1.0000e+00 |       48932 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -0.5 | 2.5716e-01 | 1.0000e+00 |       45820 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "      0.0 | 2.3105e-01 | 1.0000e+00 |       41167 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "      0.5 | 2.0566e-01 | 1.0000e+00 |       36643 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "      1.0 | 1.7111e-01 | 1.0000e+00 |       30487 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "      1.5 | 9.3728e-02 | 9.2969e-01 |       16700 |      178176 |          119 |         128 |         0.1 |reached target block errors\n",
      "      2.0 | 1.1097e-02 | 2.4805e-01 |        7909 |      712704 |          127 |         512 |         0.3 |reached target block errors\n",
      "      2.5 | 1.0694e-03 | 1.3706e-02 |       10861 |    10156032 |          100 |        7296 |         4.6 |reached target block errors\n",
      "      3.0 | 2.0177e-04 | 2.0313e-03 |        3595 |    17817600 |           26 |       12800 |         8.0 |reached max iterations\n",
      "      3.5 | 6.2130e-05 | 5.4688e-04 |        1107 |    17817600 |            7 |       12800 |         8.1 |reached max iterations\n",
      "      4.0 | 2.6715e-05 | 2.3437e-04 |         476 |    17817600 |            3 |       12800 |         8.1 |reached max iterations\n",
      "      4.5 | 1.5154e-05 | 7.8125e-05 |         270 |    17817600 |            1 |       12800 |         8.1 |reached max iterations\n",
      "      5.0 | 0.0000e+00 | 0.0000e+00 |           0 |    17817600 |            0 |       12800 |         8.1 |reached max iterations\n",
      "\n",
      "Simulation stopped as no error occurred @ EbNo = 5.0 dB.\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Dictionary storing the evaluation results\n",
    "BLER = {}\n",
    "\n",
    "model = E2ESystem('baseline-perfect-csi')\n",
    "_,bler = sim_ber(model, ebno_dbs, batch_size=128, num_target_block_errors=100, max_mc_iter=100)\n",
    "BLER['baseline-perfect-csi'] = bler.numpy()\n",
    "\n",
    "model = E2ESystem('baseline-ls-estimation')\n",
    "_,bler = sim_ber(model, ebno_dbs, batch_size=128, num_target_block_errors=100, max_mc_iter=100)\n",
    "BLER['baseline-ls-estimation'] = bler.numpy()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8a0ca3af",
   "metadata": {},
   "source": [
    "## Training the Neural Receiver <a class=\"anchor\" id=\"Training-the-Neural-Receiver\"></a>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a4e33757",
   "metadata": {},
   "source": [
    "In the next cell, one forward pass is performed within a *gradient tape*, which enables the computation of gradient and therefore the optimization of the neural network through stochastic gradient descent (SGD)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "44ac3322",
   "metadata": {},
   "source": [
    "**Note:** For an introduction to the implementation of differentiable communication systems and their optimization through SGD and backpropagation with Sionna, please refer to [the Part 2 of the Sionna tutorial for Beginners](https://nvlabs.github.io/sionna/phy/tutorials/Sionna_tutorial_part2.html)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "4df9fe26",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:25:22.153882Z",
     "iopub.status.busy": "2025-03-10T22:25:22.153628Z",
     "iopub.status.idle": "2025-03-10T22:25:27.798240Z",
     "shell.execute_reply": "2025-03-10T22:25:27.796734Z"
    }
   },
   "outputs": [],
   "source": [
    "# The end-to-end system equipped with the neural receiver is instantiated for training.\n",
    "# When called, it therefore returns the estimated BMD rate\n",
    "model = E2ESystem('neural-receiver', training=True)\n",
    "\n",
    "# Sampling a batch of SNRs\n",
    "ebno_db = tf.random.uniform(shape=[], minval=ebno_db_min, maxval=ebno_db_max)\n",
    "# Forward pass\n",
    "with tf.GradientTape() as tape:\n",
    "    rate = model(training_batch_size, ebno_db)\n",
    "    # Tensorflow optimizers only know how to minimize loss function.\n",
    "    # Therefore, a loss function is defined as the additive inverse of the BMD rate\n",
    "    loss = -rate"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fd4da949",
   "metadata": {},
   "source": [
    "Next, one can perform one step of stochastic gradient descent (SGD).\n",
    "The Adam optimizer is used"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "f5bbbea0",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:25:27.803064Z",
     "iopub.status.busy": "2025-03-10T22:25:27.802818Z",
     "iopub.status.idle": "2025-03-10T22:25:29.057353Z",
     "shell.execute_reply": "2025-03-10T22:25:29.056488Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Variable path=adam/iteration, shape=(), dtype=int64, value=1>"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "optimizer = tf.keras.optimizers.Adam()\n",
    "\n",
    "# Computing and applying gradients\n",
    "weights = tape.watched_variables()\n",
    "grads = tape.gradient(loss, weights)\n",
    "optimizer.apply_gradients(zip(grads, weights))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e83c74d5",
   "metadata": {},
   "source": [
    "Training consists in looping over SGD steps. The next cell implements a training loop.\n",
    "\n",
    "At each iteration:\n",
    "- A batch of SNRs $E_b/N_0$ is sampled\n",
    "- A forward pass through the end-to-end system is performed within a gradient tape\n",
    "- The gradients are computed using the gradient tape, and applied using the Adam optimizer\n",
    "- The achieved BMD rate is periodically shown\n",
    "\n",
    "After training, the weights of the models are saved in a file\n",
    "\n",
    "**Note:** Training can take a while. Therefore, [we have made pre-trained weights available](https://drive.google.com/file/d/1W9WkWhup6H_vXx0-CojJHJatuPmHJNRF/view?usp=sharing). Do not execute the next cell if you don't want to train the model from scratch. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "9713f72d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:25:29.061261Z",
     "iopub.status.busy": "2025-03-10T22:25:29.060953Z",
     "iopub.status.idle": "2025-03-10T22:25:29.066468Z",
     "shell.execute_reply": "2025-03-10T22:25:29.065907Z"
    }
   },
   "outputs": [],
   "source": [
    "training = False # Change to True to train your own model\n",
    "if training:\n",
    "    model = E2ESystem('neural-receiver', training=True)\n",
    "\n",
    "    optimizer = tf.keras.optimizers.Adam()\n",
    "\n",
    "    for i in range(num_training_iterations):\n",
    "        # Sampling a batch of SNRs\n",
    "        ebno_db = tf.random.uniform(shape=[], minval=ebno_db_min, maxval=ebno_db_max)\n",
    "        # Forward pass\n",
    "        with tf.GradientTape() as tape:\n",
    "            rate = model(training_batch_size, ebno_db)\n",
    "            # Tensorflow optimizers only know how to minimize loss function.\n",
    "            # Therefore, a loss function is defined as the additive inverse of the BMD rate\n",
    "            loss = -rate\n",
    "        # Computing and applying gradients\n",
    "        weights = tape.watched_variables()\n",
    "        grads = tape.gradient(loss, weights)\n",
    "        optimizer.apply_gradients(zip(grads, weights))\n",
    "        # Periodically printing the progress\n",
    "        if i % 100 == 0:\n",
    "            print('Iteration {}/{}  Rate: {:.4f} bit'.format(i, num_training_iterations, rate.numpy()), end='\\r')\n",
    "\n",
    "    # Save the weights in a file\n",
    "    weights = model._neural_receiver.weights\n",
    "    with open(model_weights_path, 'wb') as f:\n",
    "        pickle.dump(weights, f)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "966e5dfc",
   "metadata": {},
   "source": [
    "## Evaluation of the Neural Receiver <a class=\"anchor\" id=\"Evaluation-of-the-Neural-Receiver\"></a>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "114b2d31",
   "metadata": {},
   "source": [
    "The next cell evaluates the neural receiver.\n",
    "\n",
    "**Note:** Evaluation of the system can take a while and requires having the trained weights of the neural receiver. Therefore, we provide pre-computed results at the end of this notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "d07b3699",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:25:29.070409Z",
     "iopub.status.busy": "2025-03-10T22:25:29.070123Z",
     "iopub.status.idle": "2025-03-10T22:27:33.638326Z",
     "shell.execute_reply": "2025-03-10T22:27:33.637505Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "EbNo [dB] |        BER |       BLER |  bit errors |    num bits | block errors |  num blocks | runtime [s] |    status\n",
      "---------------------------------------------------------------------------------------------------------------------------------------\n",
      "     -5.0 | 2.6003e-01 | 1.0000e+00 |       46331 |      178176 |          128 |         128 |         2.7 |reached target block errors\n",
      "     -4.5 | 2.4025e-01 | 1.0000e+00 |       42806 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -4.0 | 2.2764e-01 | 1.0000e+00 |       40560 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -3.5 | 2.0312e-01 | 1.0000e+00 |       36191 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -3.0 | 1.7575e-01 | 1.0000e+00 |       31314 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -2.5 | 1.4274e-01 | 1.0000e+00 |       25433 |      178176 |          128 |         128 |         0.1 |reached target block errors\n",
      "     -2.0 | 4.2873e-02 | 8.3594e-01 |        7639 |      178176 |          107 |         128 |         0.1 |reached target block errors\n",
      "     -1.5 | 3.7594e-03 | 1.3672e-01 |        4019 |     1069056 |          105 |         768 |         0.6 |reached target block errors\n",
      "     -1.0 | 7.4014e-04 | 9.7656e-03 |       10550 |    14254080 |          100 |       10240 |         8.5 |reached target block errors\n",
      "     -0.5 | 3.1817e-04 | 2.5781e-03 |        5669 |    17817600 |           33 |       12800 |        10.7 |reached max iterations\n",
      "      0.0 | 1.0080e-04 | 9.3750e-04 |        1796 |    17817600 |           12 |       12800 |        10.7 |reached max iterations\n",
      "      0.5 | 1.1393e-04 | 9.3750e-04 |        2030 |    17817600 |           12 |       12800 |        10.7 |reached max iterations\n",
      "      1.0 | 5.6349e-05 | 3.9063e-04 |        1004 |    17817600 |            5 |       12800 |        10.7 |reached max iterations\n",
      "      1.5 | 1.5490e-05 | 2.3437e-04 |         276 |    17817600 |            3 |       12800 |        10.7 |reached max iterations\n",
      "      2.0 | 2.1888e-05 | 1.5625e-04 |         390 |    17817600 |            2 |       12800 |        10.7 |reached max iterations\n",
      "      2.5 | 1.4256e-05 | 7.8125e-05 |         254 |    17817600 |            1 |       12800 |        10.7 |reached max iterations\n",
      "      3.0 | 3.1149e-05 | 1.5625e-04 |         555 |    17817600 |            2 |       12800 |        10.7 |reached max iterations\n",
      "      3.5 | 8.9799e-07 | 7.8125e-05 |          16 |    17817600 |            1 |       12800 |        10.7 |reached max iterations\n",
      "      4.0 | 0.0000e+00 | 0.0000e+00 |           0 |    17817600 |            0 |       12800 |        10.7 |reached max iterations\n",
      "\n",
      "Simulation stopped as no error occurred @ EbNo = 4.0 dB.\n",
      "\n"
     ]
    }
   ],
   "source": [
    "model = E2ESystem('neural-receiver')\n",
    "\n",
    "# Run one inference to build the layers and loading the weights\n",
    "model(1, tf.constant(10.0, tf.float32))\n",
    "with open(model_weights_path, 'rb') as f:\n",
    "    weights = pickle.load(f)\n",
    "\n",
    "for i, w in enumerate(weights):\n",
    "    model._neural_receiver.weights[i].assign(w)\n",
    "\n",
    "# Evaluations\n",
    "_,bler = sim_ber(model, ebno_dbs, batch_size=128, num_target_block_errors=100, max_mc_iter=100)\n",
    "BLER['neural-receiver'] = bler.numpy()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a4ecc17d",
   "metadata": {},
   "source": [
    "Finally, we plots the BLERs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "48f3b83a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:27:33.641922Z",
     "iopub.status.busy": "2025-03-10T22:27:33.641670Z",
     "iopub.status.idle": "2025-03-10T22:27:34.167978Z",
     "shell.execute_reply": "2025-03-10T22:27:34.167311Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "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/vMf1q9fzwsvvEBZWRlPPPEEAC+99BI333wzM2fOZOXKlSxZsoQ77riDioqKVo012FR4i0izLjmxCxcc1xm3B2567QeKHNXNv+mQrMQsBnQYwIAOA+if2p/+Ud5Tync4djCgwwAV3SI/Z4uCC16EW9ZC15OMjkZEIozH46Gqts6nW3m1k9nvreNo153WP3fve+spr3b61J6/V7DGxMSQmZlJZmYmw4YN46677mLXrl3s27evYZs777yTnJwc4uPj6dmzJ/fcc0+jCa1WrVrFuHHjSEpKIjk5meOPP57vvvuu4fWvvvqK0aNHExcXR5cuXZg5cyaVlZVNxmSxWJg/fz7gHXW2WCy8/fbbjBs3jvj4eIYOHcrXX3/d6D1fffUVEydOJCEhwac+/NGhQwcyMzM54YQTePzxxyksLOTbb7/16bN1796d+++/nyuvvJLk5GSuv/56LBYLK1as4E9/+hMWi4V7770XgF27dnHRRRfRrl07UlNTOe+889i+fXujWF566SUGDhxITEwMWVlZzJgxo6EfgKlTp2KxWBoe/9zu3buZOXMmM2fO5KWXXmLs2LF0796dU089lb///e/88Y9/BOC9997joosu4te//jW9e/dm4MCBXHrppTz44INB2adG0anmItIsi8XCA1MGsW5vGRsLypnx2g/MvXYkdpv/3931ierD17Vfs2TvEjweT0RcCyXS6nqONToCEYlQB50uBvzxk6C05QEKHNUMvvdTn7Zf/6eziI8OrLyoqKjg3//+N7179240CpuUlMTLL79MdnY2a9as4brrriMpKYk77rgDgMsuu4zhw4fz/PPPY7PZWLlyZcPpv1u3bmXChAk88MADvPTSS+zbt48ZM2YwY8YM5syZ43Nss2bN4vHHH6dPnz7MmjWLSy+9lC1btmC329m6dSuTJk1i1qxZvPzyy5SUlATUhy/q15Sura31+bM9/vjj/PGPf2T27NkAPPLII5xxxhlMmDCB2267jcTERJxOJ2eddRYnn3wyX375JXa7nQceeIAJEyawevVqoqOjef7557n11lt55JFHmDhxImVlZSxZsgSA5cuXk56ezpw5c5gwYUKTa12/+eab1NbWNuTu59q1awd4zzpYtGgRO3bsoFu3bsHafYZT4S0iPomLtvHcZcdx7rNLWLZtP499uom7J/o/IVoPew9ibDEUVBawrWwbPdv1DEG0IibirIaowCdMEhEJVx988AGJiYkAVFZWkpWVxQcffNDo+to//OEPDT93796d2267jddff72heNu5cye33347/fr1A6BPnz4N2z/88MNcdtll3HzzzQ2vPfPMM4wZM4bnn3/e58nobrvtNs4++2zAe330wIED2bJlC/369ePhhx9m2rRp3HjjjSQnJ9O3b9+A+mhOaWkp999/P4mJiYwYMYJZs2b59NlOO+00fv/73zdqy263k5iYSGZmJgD//ve/cbvd/P3vf28YEJkzZw7t2rXjiy++4Mwzz+SBBx7g97//fcMkdAAnnngiAB07dgS8hXN9m0ezefNmkpOTyco69tmOs2fP5vzzz6d79+7k5ORw8sknM2nSJC688MKgXUNvBBXePtI63sbSWqXhoUu7GB6aMoCZ81bz10U/MqxTMmf0T/f5/U6nkyhLFMPThvNN4Tcs2rWILgldQhixBJuOxVZUugPbR3dgceyi7volEMSzQ5RHc1AezSFY63i73W7cbjcxNgtr7x3v03uXbdvPr/7Z/FwSL111PCN6pDa7XYzN4vM61h6Ph7Fjx/Lcc95lRw8cOMDzzz/PxIkT+eabbxpGOufNm9cwyVZFRQV1dXUkJyc39HPLLbdw7bXX8sorr3D66adz4YUX0quXdyLYVatWsXr1al599dVG/brdbrZu3Ur//v0bPVevfl/WPzdo0KCGnzMyMgAoKCggJyenoY+5c+ces496O3fuZNCgQQ2P77777oYJww5X398pp5yC1WqlsrKSnj178tprr9GxY0efP9vxxx9/1Jwc/plXrlzJli1bSEpKarRNdXU1mzdvZsiQIezdu5dx48YdM7+H77OmXrdYmv8dycjIYMmSJaxdu5Yvv/ySpUuXctVVV/Hiiy/y0UcfYbVaGy5rCOa66U2t4+12u4OyjrcK7yYcvo43wKeffqp1vMPAggULjA5BgDFZVhblW7l13g/cNsRFmp9f5rYvaw/Ae6vfI/XH5v8jl/CjYzH07K6DnLX9K+zuGr5582n2J/YNeh/Kozkoj+YQaB7r1/GuqKhomDzLV0MzYshIiqaovPao13lbgPSkaIZmxFBXXdVse+W+TwGD0+kkJiaG9HTvF/jp6ek88cQTvPXWW+Tm5vKHP/yBZcuWccUVV3DXXXfxwAMPkJyczNtvv82zzz6Lw+EAvIX35MmT+fTTT1mwYAH33nsv//jHPzjnnHNwOBxcffXV3HDDDUf037FjRxwOB263m+rq6ob2AA4ePIjD4WiYzKu2trbh9frnKioqcDgcPvVxuMTERBYvXtzwuH379kdsc3g///jHP+jXrx+pqamkpKQA+Nyv2+3GZrMd0b7L5aKmpqbh+f379zNs2DD+9re/HdFWhw4dGorLqqqqo8Zar36/NaVr166UlZWRl5d3zJHxw7e/7LLLuOyyy7j88suZNGkSH330EaNHj6ampgaXy3XM/gJVXl7e6HFtbS0HDx5k8eLFR13H21cqvJswffp0pk+f3rCO95lnnql1vA2ktUrDy3iXm8tf+o7vd5byVn573rh+BLFRR7+e53D1ebxqzFV89MlH7HTvZNyZ44izx7VC1BIMOhZbl5VFsHouo+K24Zp0S9DaVR7NQXk0h2Ct452YmBjQac2zJw9k+twfsECj4tty2Ovt26X43W5zoqKisNvtjf6+drvdWK1W3G43ycnJrF69mm7duvGnP/2pYZvnnnsOi8XS6H3HHXccxx13HHfddRfTpk1j3rx5TJs2jeOPP56tW7cybNiwJuOwWq3ExsY2ai8uLo7k5OSG0+ATEhIaXq8fCY2Pj2+YzG3Lli307NnT53W8U1ObH3So77tv374MHTr0iNcD/WzgXas8Jiam4fmRI0cyf/58evbs2WS90717d7755puGU+5/Lioqiujo6GPWS5dddhn33XcfL7zwAk8++eQRr5eWljZc5/1z9ae1ezwekpOTiYmJwWazBbU+O9Y63nFxcZx66qlHXcfbVyq8faQ1MsOD8hAeoqIg97LjOPuZr9hQUM6DH+XxyAW+r7nZJ7UPWQlZ5Ffms6pkFaM7jw5htBIKOhZbyQlXweq5WDe8i/XsRyEmqfn3+EF5NAfl0RyMWsd70pBsnrdajljHOzPE63hbLBZqa2spKioCvKeaP/vss1RUVHDuueditVrJyclh586dvPHGG5x44ol8+OGHDTOOW61WDh48yO23386FF15Ijx492L17N9999x0XXHABVquVu+66i5NOOomZM2dy7bXXkpCQwPr161mwYAHPPvtso1h+vm7z4fvz5z8f/lx9H7fffjs33ngjSUlJR+3DX0fr+3CBfrajPX/FFVfwxBNPMHXqVP70pz/RuXNnduzYwdtvv80dd9xB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JTYRUzhtFvQaB5rkR6TNqnJWNXurrqsOWruBOOOMM8jMzOThhx8+5nZfffUVo0ePJi4uji5dujBz5kwqK3/64t5isTB//vxG72nXrh0vv/wyANu3b8disTBv3jzGjBlDbGwsr776KiUlJVx66aV06tSJ+Ph4Bg8ezGuvvebXZ7j33ns57rjj+Ne//kWvXr0aZr0uLS3l2muvpWPHjiQnJ3PaaaexatWqRu99//33OfHEE4mNjSUtLY2pU6c2vFZTU8Ntt91Gp06dSEhIYOTIkXzxxRcNrx9+qnleXh4Wi4WNGzc2av+pp56iV69eDY/Xrl3LxIkTSUxMJCMjgyuuuILi4uKG18eOHcuMGTO4+eabSUtL46yzzvJrX8ix6VRzETFETmYS3+04QF5BOQxtertR2aNYtW8VX+35ivP7nN96AYqIiESwkXNHhqTdCf+ZcNQR8DVXrfG7LZvNxkMPPcS0adOYOXMmnTt3PmKbrVu3MmHCBB544AFeeukl9u3bx4wZM5gxYwZz5szxq7+77rqLJ554guHDhxMbG0t1dTXHH388d955J8nJyXz44YdcccUV9OrVixEjRvjc7pYtW3jvvfd46623Gma5/uUvf0lcXBwfffQRKSkp/PWvf+X0008nLy+P1NRUPvzwQ6ZOncqsWbP417/+RW1tLf/9738b2pwxYwbr16/n9ddfJzs7m3feeYcJEyawZs0a+vTp06j/nJwcTjjhBF599VXuv//+hudfffVVpk2bBni/CDjttNO49tpreeqppzh48CB33nknF110EZ9//nnDe/75z39y4403smTJEr/2rTRPhbeIGMKXEW+AUZ1G8dyq5/hm7zfUueuwW/XPlohPDuyAZX8DixXOvL/57UVEDDB16lSGDRvG7Nmz+cc//nHE6w8//DCXXXZZwwRnffr04ZlnnmHMmDE8//zzfq1dfvPNN3P++Y2/xL/tttsafr7pppv45JNPeOONN/wqvGtra3nhhRfo2bMnVquVr776imXLllFUVERMTAwAjz/+OPPnz+ett97i+uuv58EHH+SSSy7hvvvua2hn6FDvSMTOnTuZM2cOO3fuJDs7uyHOjz/+mDlz5vDQQw8dEcNll13Gs88+21B45+XlsWLFCv79738D8OyzzzJ8+PBG733ppZfo0qULeXl55OTkAN79++ijj/r82cV3+gtW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ww/zu9/9DoD8/HwSEhJ4++23ufHGG+t9LI14i+Xk5ORw11138Ze//IXw8HDTcaSB5OfnExMTYzqGnEJZWRmZmZlccsklVduCgoK45JJL+OabbwwmkzORn58PoM+eBY0ZM4Yrrrii2mdSrGPBggX079+fG264gfj4eM455xxef/1107GkjgYNGsSyZcvYvn07ABs2bODLL7/ksssuM5xM6mvPnj1kZ2dX+7c1Ojqa1NTUM/55x3Gm4UQak9fr5dZbb+Xee++lf//+/PDDD6YjSQPYuXMnM2bMYNq0aaajyCnk5ubidrtJSEiotj0hIYFt27YZSiVnwuPxMG7cOAYPHkzPnj1Nx5E6eP/998nKymLt2rWmo0g97d69m1mzZjF+/Hgee+wx1q5dy29/+1uCg4MZPXq06XhSS+np6RQUFNC1a1fsdjtut5vJkydz8803m44m9ZSdnQ1w0p93Ku+rL414i19IT0/HZrPVeNu2bRszZsygsLCQiRMnmo4sJ1HbOv7cvn37GD58ODfccAN33XWXoeQiTc+YMWPYtGkT77//vukoUgd79+7lwQcf5L333iM0NNR0HKknj8dD3759ee655zjnnHO4++67ueuuu5g9e7bpaFIHH374Ie+99x5z584lKyuLd955h2nTpvHOO++YjiZ+SCPe4hcefvhhbr311hr36dChA59//jnffPMNISEh1e7r378/N998s/6hM6y2day0f/9+0tLSGDRoEK+99pqP08mZiIuLw263k5OTU217Tk4OiYmJhlJJfT3wwAMsXLiQlStXctZZZ5mOI3WQmZnJwYMH6du3b9U2t9vNypUrmTlzJqWlpdjtdoMJpTaSkpLo3r17tW3dunXj448/NpRI6uORRx4hPT296rrfXr168eOPP/L888/rzAWLqvyZJicnh6SkpKrtOTk5ZzyflBpv8QstW7akZcuWp93vlVde4dlnn636fv/+/QwbNowPPviA1NRUX0aUWqhtHaFipDstLY1+/frx1ltvERSkE3D8WXBwMP369WPZsmVVE5B4PB6WLVvGAw88YDac1JrX62Xs2LHMmzeP5cuX0759e9ORpI4uvvhivvvuu2rbbrvtNrp27cqjjz6qptsiBg8efMJSftu3b6dt27aGEkl9lJSUnPDzi91ux+PxGEokZ6p9+/YkJiaybNmyqka7oKCA1atXc999953Rc6vxFktp06ZNte8jIyMB6Nixo0ZtLGTfvn0MGTKEtm3bMm3aNA4dOlR1n0ZP/df48eMZPXo0/fv3Z+DAgUyfPp3i4mJuu+0209GklsaMGcPcuXOZP38+zZo1q7peLTo6mrCwMMPppDaaNWt2wjX5ERERxMbG6lp9C3nooYcYNGgQzz33HL/85S9Zs2YNr732ms7+spgRI0YwefJk2rRpU7XKzksvvcTtt99uOprUoKioiJ07d1Z9v2fPHtavX09MTAxt2rRh3LhxPPvss3Tu3Jn27dvz5JNPkpycXDXwUF9qvEWk0S1dupSdO3eyc+fOE35hohUO/devfvUrDh06xFNPPUV2djYpKSksXrz4hAlIxH/NmjULgCFDhlTb/tZbb532MhERaTgDBgxg3rx5TJw4kWeeeYb27dszffp0TcplMTNmzODJJ5/k/vvv5+DBgyQnJ3PPPffw1FNPmY4mNVi3bh1paWlV348fPx6A0aNH8/bbbzNhwgSKi4u5++67OXr0KOeffz6LFy8+43k1tI63iIiIiIiIiA/pokoRERERERERH1LjLSIiIiIiIuJDarxFREREREREfEiNt4iIiIiIiIgPqfEWERERERER8SE13iIiIiIiIiI+pMZbRERERERExIfUeIuIiIiIiIj4kBpvERERERERER9S4y0iIiIN6vDhw8THx/PDDz+ccp8hQ4Ywbty4Oj/3jTfeyIsvvlj/cCIiIgao8RYREbGQCy+8EJvNdsLtlltuqdXjb7vtNp544okTnu+vf/1rtf1mzJhBcnJyvTJOnjyZq6++mnbt2tX6Mbfeemu11xMbG8vw4cPZuHFjtf2eeOIJJk+eTH5+fr2yiYiImKDGW0RExCK8Xi/ffvst06ZN48CBA9Vur7766mkf73a7WbhwIVdddVW150tKSuLjjz+utm9mZiZ9+/atc8aSkhLefPNN7rjjjjo/dvjw4VWvZ9myZTgcDq688spq+/Ts2ZOOHTvy7rvv1vn5RURETFHjLSIiYhE7duygsLCQCy64gMTExGq3yMjI0z7+66+/xul0MmDAgGrP98QTT7Bo0SJKSkqq9s3KyqJfv351zviPf/yDkJAQzj333KptxcXF3HLLLURGRpKUlHTKU8VDQkKqXk9KSgrp6ens3buXQ4cOVdtvxIgRvP/++3XOJiIiYooabxEREYvIzMzE4XDQu3fvej1+wYIFjBgxApvNVvV8oaGh3HnnnURFRbFo0SIAjh8/ztatW+s14r1q1aoTGvZHHnmEFStWMH/+fJYsWcLy5cvJysqq8XmKiop499136dSpE7GxsdXuGzhwIGvWrKG0tLTO+URERExQ4y0iImIRWVlZuN1uYmNjiYyMrLrdc889APz973/ngQceOOXj58+fX3WaeeXz9e7dm+DgYK655ho++ugjADZs2EB5eXlV471w4ULOPvtsOnfuzBtvvFFjxh9//LHateFFRUW8+eabTJs2jYsvvphevXrxzjvvUF5efsJjFy5cWPWamjVrxoIFC/jggw8ICqr+40pycjJlZWVkZ2ef5h0TERHxDw7TAURERKR2srKyuOmmm3j66aerbY+JiQFg48aNpKSknPSxW7duZf/+/Vx88cXVnq+yub722mu59tprKS0tJSsri5YtW9K6dWvKy8sZP348X3zxBdHR0fTr149rrrnmhFHoSseOHSM0NLTq+127dlFWVkZqamq1vGefffYJj01LS2PWrFkA5OXl8eqrr3LZZZexZs0a2rZtW7VfWFgYQLVT40VERPyZRrxFREQsIisri8GDB9OpU6dqt5833tu2baNfv350796dbdu2VT12wYIFDB06tFpT/PPruIcMGYLT6eSzzz6rNrHamjVr6NGjB61atSIyMpLLLruMJUuWnDJjXFwceXl59Xp9ERERVa9pwIABvPHGGxQXF/P6669X2+/IkSMAtGzZsl7HERERaWxqvEVERCxg9+7dHD16lD59+pxyn40bN9K6dWsyMzMZN24c06ZNq7pv/vz5XH311Sc8X2WD7XA4uOqqq/j444+rNeT79++nVatWVY9r1aoV+/btO2WGc845hy1btlR937FjR5xOJ6tXr67alpeXx/bt20/7mm02G0FBQRw7dqza9k2bNnHWWWcRFxd32ucQERHxB2q8RURELCAzMxOAhIQEsrOzq908Hg+lpaWUlJQwduxYAFJSUsjNzQXg4MGDrFu3rtrSXJmZmQQHB9OzZ8+qbddddx0LFixg8+bN9ZpYDWDYsGFs3ry5atQ7MjKSO+64g0ceeYTPP/+cTZs2ceutt55w3TZAaWlp1WvaunUrY8eOpaioiBEjRlTbb9WqVVx66aX1yiciImKCrvEWERGxgMpZwDt37lxte0hICAUFBWzZsoVu3bpVNbSVE6cBfPrppwwcOLDaCHFWVhY9e/YkODi4atvQoUNxu92UlZVVNd7JycnVRrj37dvHwIEDT5mzV69e9O3blw8//LBq0repU6dWNdDNmjXj4YcfJj8//4THLl68mKSkJACaNWtG165d+dvf/saQIUOq9jl+/DiffPIJixcvPv2bJiIi4idsXq/XazqEiIiInJl33nmH5557jk2bNpGXl8fll1/OokWLaNmyJVdddRXnn38+EyZMqPPzlpeX061bN5YvX141udrXX399ysnVoGJ29UceeYRNmzaddGT7TMyaNYt58+bVeJ25iIiIv9GIt4iISADYuHEjV155JQMGDMDtdvPSSy9VTT52/vnnc9NNN9XreR0OBy+++CJpaWl4PB4mTJhQY9MNcMUVV7Bjxw727dtH69at63XcU3E6ncyYMaNBn1NERMTXNOItIiIiIiIi4kOaXE1ERERERETEh9R4i4iIiIiIiPiQGm8RERERERERH1LjLSIiIiIiIuJDarxFREREREREfEiNt4iIiIiIiIgPqfEWERERERER8SE13iIiIiIiIiI+pMZbRERERERExIfUeIuIiIiIiIj4kBpvERERERERER/6f8It7lYravwUAAAAAElFTkSuQmCC",
      "text/plain": [
       "<Figure size 1000x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "plt.figure(figsize=(10,6))\n",
    "# Baseline - Perfect CSI\n",
    "plt.semilogy(ebno_dbs, BLER['baseline-perfect-csi'], 'o-', c=f'C0', label=f'Baseline - Perfect CSI')\n",
    "# Baseline - LS Estimation\n",
    "plt.semilogy(ebno_dbs, BLER['baseline-ls-estimation'], 'x--', c=f'C1', label=f'Baseline - LS Estimation')\n",
    "# Neural receiver\n",
    "plt.semilogy(ebno_dbs, BLER['neural-receiver'], 's-.', c=f'C2', label=f'Neural receiver')\n",
    "#\n",
    "plt.xlabel(r\"$E_b/N_0$ (dB)\")\n",
    "plt.ylabel(\"BLER\")\n",
    "plt.grid(which=\"both\")\n",
    "plt.ylim((1e-4, 1.0))\n",
    "plt.legend()\n",
    "plt.tight_layout()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d0dd52db",
   "metadata": {},
   "source": [
    "## Pre-computed Results <a class=\"anchor\" id=\"Pre-computed-Results\"></a>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "5ca9dd96",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-03-10T22:27:34.170715Z",
     "iopub.status.busy": "2025-03-10T22:27:34.170384Z",
     "iopub.status.idle": "2025-03-10T22:27:34.174638Z",
     "shell.execute_reply": "2025-03-10T22:27:34.173586Z"
    }
   },
   "outputs": [],
   "source": [
    "pre_computed_results = \"{'baseline-perfect-csi': [1.0, 1.0, 1.0, 1.0, 1.0, 0.9916930379746836, 0.5367080479452054, 0.0285078125, 0.0017890625, 0.0006171875, 0.0002265625, 9.375e-05, 2.34375e-05, 7.8125e-06, 1.5625e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'baseline-ls-estimation': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9998022151898734, 0.9199448529411764, 0.25374190938511326, 0.0110234375, 0.002078125, 0.0008359375, 0.0004375, 0.000171875, 9.375e-05, 4.6875e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 'neural-receiver': [1.0, 1.0, 1.0, 1.0, 1.0, 0.9984177215189873, 0.7505952380952381, 0.10016025641025642, 0.00740625, 0.0021640625, 0.000984375, 0.0003671875, 0.000203125, 0.0001484375, 3.125e-05, 2.34375e-05, 7.8125e-06, 7.8125e-06, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]}\"\n",
    "BLER = eval(pre_computed_results)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b2ee96b0",
   "metadata": {},
   "source": [
    "## References <a class=\"anchor\" id=\"References\"></a>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd33c7e9",
   "metadata": {},
   "source": [
    "[1] M. Honkala, D. Korpi and J. M. J. Huttunen, \"DeepRx: Fully Convolutional Deep Learning Receiver,\" in IEEE Transactions on Wireless Communications, vol. 20, no. 6, pp. 3925-3940, June 2021, doi: 10.1109/TWC.2021.3054520.\n",
    "\n",
    "[2] F. Ait Aoudia and J. Hoydis, \"End-to-end Learning for OFDM: From Neural Receivers to Pilotless Communication,\" in IEEE Transactions on Wireless Communications, doi: 10.1109/TWC.2021.3101364.\n",
    "\n",
    "[3] Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun, \"Deep Residual Learning for Image Recognition\", Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (CVPR), 2016, pp. 770-778\n",
    "\n",
    "[4] G. Böcherer, \"Achievable Rates for Probabilistic Shaping\", arXiv:1707.01134, 2017."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.16"
  },
  "widgets": {
   "application/vnd.jupyter.widget-state+json": {
    "state": {},
    "version_major": 2,
    "version_minor": 0
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
